package util.jobs;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import models.CNAverage;
import models.NodeConfig;
import models.SensorConfig;
import play.Logger;
import play.jobs.Every;
import play.jobs.Job;
import util.Persistence;
import util.PlausabilityCalculator;
import util.UnitConverter;
import ch.bfh.abch.rems.REMSMulticastController;
import ch.bfh.abch.rems.sender.tinkerforge.TFMulticastSender;
import ch.bfh.abcm.rems.gateway.GateWayMessageType;
import ch.bfh.abcm.rems.gateway.GatewayMessage;
import ch.bfh.abcm.rems.sensors.tinkerforge.TFSensorType;

@Every("6s")
public class PeriodicNotificationJob extends Job {

    private static final String UNKNOWN = "unbekannt";
    private static final long NOTIF_INTERVAL_MS = 60 * 1000;
    private static final long CNA_CREATE_INTERVALL_MS = 3 * 1000;

    private static final String NEW_LINE = "\r\n";

    private TFMulticastSender sender = new REMSMulticastController().getMulticastSender();

    private static final org.apache.log4j.Logger NOTIFLOGGER = org.apache.log4j.Logger
            .getLogger("periodicNotification");
    private static final long MISSING_TOLERANCE = 5;

    @Override
    public void doJob() throws Exception {
        List<PeriodicNotifictionMessageElement> messageElements = createMessageElements();
        String message = formatMessge(messageElements);
        sendMulticastMessage(message);
    }

    private void sendMulticastMessage(String message) {
        GatewayMessage gatewayMessage = new GatewayMessage(GateWayMessageType.PERIODIC_NOTIFICATION, message, null);
        NOTIFLOGGER.info("Send Periodic Notification: " + gatewayMessage.getMessage());
        sender.sendGateWayMessage(gatewayMessage);
    }

    private String formatMessge(List<PeriodicNotifictionMessageElement> messageElements) {
        String message = "";
        for (PeriodicNotifictionMessageElement periodicNotifictionMessageElement : messageElements) {
            message += periodicNotifictionMessageElement.getElementText();
            message += NEW_LINE;
        }
        return message;
    }

    private List<PeriodicNotifictionMessageElement> createMessageElements() {
        List<PeriodicNotifictionMessageElement> messageElements = new ArrayList<PeriodicNotifictionMessageElement>();
        List<TFSensorType> sensorTypes = SensorConfig.getConfiguredSensorTypeList();
        List<String> nodes = NodeConfig.getNodes();
        for (TFSensorType sensorType : sensorTypes) {
            // AverageElement ae = getNotifIntervalAverage(sensorType, nodes);
            AverageElement ae = getNotifIntervalAverageElement(sensorType, nodes);
            messageElements
                    .add(new PeriodicNotifictionMessageElement(sensorType, ae.getAverage(), ae.getPlausability()));
        }
        return messageElements;
    }

    private AverageElement getNotifIntervalAverageElement(TFSensorType sensorType, List<String> nodes) {
        try {
            long now = new Date().getTime();
            long startNotifIntervallTimeStamp = now - NOTIF_INTERVAL_MS;
            ArrayList<CNAverage> averages = getAllCNAforSensorTypSince(sensorType, startNotifIntervallTimeStamp, nodes);

            if (averages.size() == 0) {
                NOTIFLOGGER.error("No averages found... returning UNKNOWN");
                return new AverageElement(UNKNOWN, 0);
            }

            int tinkerforgePeriodivNotifValue = calculateTinkerforgePeriodivNotifValue(averages);
            NOTIFLOGGER.debug(sensorType + " tinkerforgePeriodivNotifValue: " + tinkerforgePeriodivNotifValue);

            double valuePlausibility = calcValuePlausinility(averages);
            NOTIFLOGGER.debug(sensorType + " valuePlausibility: " + valuePlausibility);

            double nodePlausibility = calcNodePlausability(sensorType, startNotifIntervallTimeStamp, nodes);
            NOTIFLOGGER.debug(sensorType + " nodePlausibility: " + nodePlausibility);

            int effectivPlausibility = PlausabilityCalculator.consolidatePossabilities(valuePlausibility,
                    nodePlausibility);
            String normValue = UnitConverter.convertTFValuetoNormValue(sensorType, tinkerforgePeriodivNotifValue + "");
            return new AverageElement(normValue, effectivPlausibility);

        } catch (Exception e) {
            NOTIFLOGGER.error(sensorType + " Calculate Notification Average failed: " + e.getMessage() + ", returning "
                    + UNKNOWN);
            return new AverageElement(UNKNOWN, 0);
        }
    }

    private int calcNodePlausability(TFSensorType sensorType, long since, List<String> nodes) {
        // acutal / expected
        int expectedAmountOfCNAsForEachNode = (int) ((NOTIF_INTERVAL_MS / CNA_CREATE_INTERVALL_MS) - MISSING_TOLERANCE);
        NOTIFLOGGER.debug(sensorType + " expectedAmountOfCNAsForEachNode: " + expectedAmountOfCNAsForEachNode);
        int amountFactor = 0;
        for (String node : nodes) {
            int actualAmountOFCNAs = Persistence.getCNAsince(since, sensorType, node).size();
            NOTIFLOGGER.debug(sensorType + " actualAmountOFCNAs " + actualAmountOFCNAs);
            int singleNodefactor = (int) (((double) actualAmountOFCNAs / (double) expectedAmountOfCNAsForEachNode) * 100);
            NOTIFLOGGER.debug(sensorType + " singleNodefactor " + singleNodefactor);
            if (singleNodefactor >= 100) {
                amountFactor += 100;
            } else {
                amountFactor += singleNodefactor;
            }
        }
        int nodesPlausibility = amountFactor / nodes.size();
        NOTIFLOGGER.debug(sensorType + " amountFactor: " + amountFactor + " nodes.size(): " + nodes.size()
                + " nodesPlausibility: " + nodesPlausibility);
        return nodesPlausibility;
    }

    private double calcValuePlausinility(ArrayList<CNAverage> averages) {
        // Size of averages is > 0, we checked this before...
        double posab = 0d;
        for (CNAverage cnAverage : averages) {
            posab += cnAverage.getPlausability();
        }
        try {
            return posab / averages.size();
        } catch (Exception e) {
            NOTIFLOGGER.error("something went wery wrong... " + e.getMessage());
            return 0d;
        }
    }

    private int calculateTinkerforgePeriodivNotifValue(ArrayList<CNAverage> averages) {
        int sum = 0;
        for (CNAverage cnAverage : averages) {
            sum += cnAverage.getNodeValue();
        }
        try {
            return (int) (sum / averages.size());
        } catch (Exception e) {
            NOTIFLOGGER.error("something went wery wrong... " + e.getMessage());
            return -99999;
        }

    }

    private ArrayList<CNAverage> getAllCNAforSensorTypSince(TFSensorType sensorType, long since, List<String> nodes) {
        ArrayList<CNAverage> rv = new ArrayList<CNAverage>();
        for (String node : nodes) {
//            List<CNAverage> cnAverages = CNAverage.getCNAsince(since, sensorType, node);
            List<CNAverage> cnAverages = Persistence.getCNAsince(since, sensorType, node);
            rv.addAll(cnAverages);
        }
        return rv;
    }

    private class AverageElement {

        private String average;
        private int plausability;

        public AverageElement(String average, int plausability) {
            this.average = average;
            this.plausability = plausability;
        }

        public String getAverage() {
            return average;
        }

        public double getPlausability() {
            return plausability;
        }
    }

    private class PeriodicNotifictionMessageElement {

        private TFSensorType sensorType;
        private String value;
        private double plausability;

        public PeriodicNotifictionMessageElement(TFSensorType sensorType, String value, double plausability) {
            this.sensorType = sensorType;
            this.value = value;
            this.plausability = plausability;
        }

        private String getElementText() {
            return sensorType + ": " + value + " " + sensorType.getUnit() + " (" + plausability + "%)";
        }

        @Override
        public String toString() {
            return getElementText();
        }

    }

}
